﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ArithmeticLibrary.Tree;
using ArithmeticLibrary.Visitors;

namespace ArithmeticLibrary
{
    /// <summary>
    /// Trida reprezentujici aritmeticke vyrazy.
    /// </summary>
    public class Expression
    {
        #region Fields

        /// <summary>
        /// Pomocna promenna.
        /// </summary>
        private int num;

        #endregion // Fields

        #region Properties

        /// <summary>
        /// Vrati koren vyrazoveho stromu.
        /// </summary>
        public Node Root { get; set; }

        /// <summary>
        /// Promenne ve vyrazu.
        /// </summary>
        public List<string> Variables { get; set; }

        #endregion // Properties

        #region Constructors

        /// <summary>
        /// vytvori instanci aritmetickeho vyrazu.
        /// </summary>
        public Expression()
        {
            this.num = 0;
            this.Root = null;
            this.Variables = new List<string>();
        }

        /// <summary>
        /// Vytvori instanci aritmetickeho vyrazu z retezcoveho vstupu.
        /// </summary>
        /// <param name="input">Vstup v retezci.</param>
        public Expression(string input)
        {
            this.num = 0;
            this.Variables = new List<string>();

            // prevedeme vstup na pole tokenu v postfixnim poradi
            string[] postfix = ToPostfix(input);

            num = postfix.Length - 1;

            // chyba prazdneho radku
            if (num < 0)
            {
                throw new Exception("CHYBA");
            }

            // z postfixniho vyrazu postavime aritmeticky strom
            this.Root = BuildTree(postfix);
        }

        #endregion // Constructors

        #region Public Methods

        /// <summary>
        /// Vyhodnoti vyraz v int aritmetice.
        /// </summary>
        public double Evaluate(double x)
        {
            return Root.Evaluate(x);
        }

        /// <summary>
        /// Zderivuje aritmeticky strom.
        /// </summary>
        /// <returns>Vrati novou instanci aritmetickeho vyrazu po zderivovani.</returns>
        public Expression Derivate()
        {
            Expression exp = new Expression();
            exp.Variables = this.Variables;

            // zderivujeme vyraz
            exp.Root = new DerivatorVisitor().Derivate(Root);
            
            // zjednodusime vyraz
            exp.Reduce();

            return exp;
        }

        /// <summary>
        /// Zjednodusi aritmeticky vyraz.
        /// </summary>
        public void Reduce()
        {
            Root = new ReducerVisitor().ReduceExpressionTree(Root);
        }

        /// <summary>
        /// Vrati aritmeticky vyraz v retezci.
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            return new InfixPriorityPrinterVisitor().ExpressionToString(Root);
        }

        #endregion // Public Methods

        #region Internal Methods

        /// <summary>
        /// Prevede infixovy vyraz do postfixoveho vyrazu.
        /// </summary>
        /// <param name="input">Vstupni retezec.</param>
        /// <returns>Vrati pole retezcu tokenu v postfixovem poradi.</returns>
        private string[] ToPostfix(string input)
        {
            Stack<string> stack = new Stack<string>();
            List<string> postfix = new List<string>();

            // pripravime vstup na parsovani
            string[] infix = PrepareInput(input);
            // identifikujeme tokeny unarniho minusu a oznacime je tokenem '~'
            FindUnaryMinuses(infix);

            // dokud jsme nedocetli cely input
            for (int i = 0; i < infix.Length; i++)
            {
                string token = infix[i];

                // pokud jsme precetli operator
                if (IsOperator(token))
                {
                    // kdyz prazdny stack, hodime tam operator
                    if (stack.Count == 0 || stack.Peek() == "(")
                        stack.Push(token);
                    // jinak pokud cteny operator ma vyssi prioritu nez operator na stacku
                    else if (Priority(token) > Priority(stack.Peek()))
                        stack.Push(token);
                    // jinak pokud cteny operator na stejnou nebo nizsi prioritu nez na stacku
                    else
                    {
                        while (stack.Count != 0 && Priority(token) <= Priority(stack.Peek()))
                            postfix.Add(stack.Pop());

                        stack.Push(token);
                    }
                }
                // jinak pokud jsme narazili na levou zavorku
                else if (token == "(")
                {
                    stack.Push(token);
                }
                // jinak pokud jsme narazili na pravou zavorku
                else if (token == ")")
                {
                    // vsechny operatory do leve zavorky dame do postfixu
                    string s = stack.Pop();
                    while (s != "(")
                    {
                        postfix.Add(s);
                        s = stack.Pop();
                    }
                }
                // jinak jsme precetli operand
                else
                {
                    // posleme ho na do postfixu
                    postfix.Add(token);
                }
            }

            while (stack.Count > 0)
                postfix.Add(stack.Pop());
            
            return postfix.ToArray();
        }

        /// <summary>
        /// Vrati prioritu operatoru vstupniho operatoru zadaneho v retezci.
        /// </summary>
        /// <param name="op">Operator</param>
        /// <returns>Vrati prioritu operatoru.</returns>
        private int Priority(string op) 
        {
            if (op == "sin" || op == "cos" || op == "tan" || op == "log" || op == "exp" || op == "~")
                return 4;
            else if (op == "^")
                return 3;
            else if (op == "*" || op == "/")
                return 2;
            else if (op == "+" || op == "-")
                return 1;
            else 
                return 0;
        }
        
        /// <summary>
        /// Pripravi vstupni rezetec pro postfixovani.
        /// </summary>
        /// <param name="input">Vstupni retezec.</param>
        /// <returns>Pole tokenu.</returns>
        private string[] PrepareInput(string input)
        { 
            StringBuilder sb = new StringBuilder();
            List<string> list = new List<string>();
        
            // pro kazdou zavorku vlozime mezeru vpred a mezeru vzad
            for (int i = 0; i < input.Length; i++) {
                if (input[i] == '(') sb.Append(" ( ");
                else if (input[i] == ')') sb.Append(" ) ");
                else if (input[i] == '+') sb.Append(" + ");
                else if (input[i] == '-') sb.Append(" - ");
                else if (input[i] == '*') sb.Append(" * ");
                else if (input[i] == '/') sb.Append(" / ");
                else if (input[i] == '^') sb.Append(" ^ ");
                //else if (input[i] == '~') sb.Append(" ~ ");
                else if (input[i] == '.')
                {
                    sb.Append(" .");
                    ++i;
                    sb.Append(input[i]);
                    sb.Append(" ");
                }
                else sb.Append(input[i]);

                sb.Replace("log", " log ");
                sb.Replace("exp", " exp ");
                sb.Replace("sin", " sin ");
                sb.Replace("cos", " cos ");
                sb.Replace("tan", " tan ");

                // nahradime konstantu pi za jeho ciselnou hodnotu
                sb.Replace("pi", " " + Math.PI.ToString() + " ");
            }
        
            // rozsekame vstup na tokeny
            string[] sep = new string[] { " " };
            string[] tokens = sb.ToString().Split(sep, StringSplitOptions.RemoveEmptyEntries);

            // zkontrolujeme uzavorkovani
            CheckBrackets(tokens);

            return tokens;
        }

        /// <summary>
        /// Identifikuje unarni minusy a oznaci je znakem '~'.
        /// </summary>
        /// <param name="tokens">Pole tokenu.</param>
        private void FindUnaryMinuses(string[] tokens)
        {
            // jestli je prvni znak minus, pak je unarni
            if (tokens[0] == "-") tokens[0] = "~";

            for (int i = 1; i < tokens.Length; i++)
            {
                // nasli jsme minus
                if (tokens[i] == "-")
                { 
                    // jestli nalezene minus lezi za levou zavorkou nebo za operatorem
                    if (tokens[i - 1] == "(" || IsOperator(tokens[i - 1]))
                        // zmenime ho na token unarniho minusu
                        tokens[i] = "~";
                }
            }
        }

        /// <summary>
        /// Zkontroluje, jestli chybi zavorky po sin, cos, tan, log nebo exp.
        /// </summary>
        /// <param name="tokens">Pole retezci s tokenama.</param>
        private void CheckBrackets(string[] tokens)
        {
            for (int i = 0; i < tokens.Length; i++)
            {
                if (IsUnaryOperator(tokens[i]) && tokens[i] != "~")
                {
                    if (tokens[i + 1] != "(")
                        throw new Exception("Chybný výraz: chybí závorka po " + tokens[i]);
                }
            }
        }

        /// <summary>
        /// Postavi aritmeticky strom z postfixoveho vyrazu.
        /// </summary>
        /// <param name="postfix">Pole retezcu v postfixovem tvaru.</param>
        /// <returns></returns>
        private Node BuildTree(string[] arr)
        {
            if (num < 0)
                throw new Exception("Chybný výraz: chybí operand.");

            string token = arr[num];
            num--;

            // pokud zpracovavany token neni operator
            // je to hodnota nebo promenna
            if (!IsOperator(token))
            {
                try
                {
                    // zkusime, jestli jde token zparsovat na double
                    // jestli ano, tak je to cislo
                    return new NumberNode(double.Parse(token));
                }
                catch (FormatException)
                {
                    // jestli ne, tak overime, jestli se jedna o pismenko
                    char c;
                    if (!char.TryParse(token, out c))
                        throw new Exception("Chybný výraz");

                    if (!char.IsLetter(c))
                        throw new Exception("Chybný výraz");

                    if (!Variables.Contains(token))
                        Variables.Add(token);

                    return new VariableNode(token);
                }
            }
            // jinak je to operator
            else
            {
                // rekurzivne zpracujeme napred deti nynejsiho operatoru

                // jestli mame binarni operator
                if (IsBinaryOperator(token))
                {
                    /// vytvori binarni operator
                    // napred vyhodnoti deti
                    Node right = BuildTree(arr);
                    Node left = BuildTree(arr);

                    BinaryOperatorNode op = null;
                    if (token == "+") op = new PlusNode();
                    else if (token == "-") op = new MinusNode();
                    else if (token == "*") op = new MultiplyNode();
                    else if (token == "/") op = new DivideNode();
                    else if (token == "^") op = new PowerNode();

                    // pripojime deti
                    op.Right = right;
                    op.Left = left;

                    return op;
                }
                else if (IsUnaryOperator(token))
                {
                    /// vytvori unarni operator
                    // napred vyhodnotim dite
                    Node child = BuildTree(arr);

                    UnaryOperatorNode op = null;
                    if (token == "~") op = new UnaryMinusNode();
                    else if (token == "sin") op = new SineNode();
                    else if (token == "cos") op = new CosineNode();
                    else if (token == "tan") op = new TangentNode();
                    else if (token == "log") op = new LogarithmNode();
                    else if (token == "exp") op = new ExponentialNode();

                    // pripojim dite
                    op.Child = child;

                    return op;
                }
                else
                    return null;
            }
        }

        /// <summary>
        /// Vyhodnoti, jestli je vstupni retezec operator.
        /// </summary>
        /// <param name="s">Vstupni retezec.</param>
        /// <returns>Vrati TRUE, pokud je vstupni retezec operator, jinak vrati FALSE.</returns>
        private bool IsOperator(string s)
        {
            if (s == "+" || s == "-" || s == "*" || s == "/" || s == "^" ||
                s == "cos" || s == "sin" || s == "tan" || s == "log" || s == "exp" || s == "~")
                return true;
            else
                return false;
        }

        /// <summary>
        /// Vyhodnoti, jestli je zadany retezec unarni operator.
        /// </summary>
        /// <param name="s">Vstupni retezec.</param>
        /// <returns>Vrati TRUE, pokud je unarni operator, jinak vrati FALSE.</returns>
        private bool IsUnaryOperator(string s)
        {
            if (s == "cos" || s == "sin" || s == "tan" || s == "log" || s == "exp" || s == "~")
                return true;
            else
                return false;
        }

        /// <summary>
        /// Vyhodnoti, jestli je zadany retezec binarni operator.
        /// </summary>
        /// <param name="s">Vstupni retezec.</param>
        /// <returns>Vrati TRUE, pokud je binarni operator, jinak vrati FALSE.</returns>
        private bool IsBinaryOperator(string s)
        {
            if (s == "+" || s == "-" || s == "*" || s == "/" || s == "^")
                return true;
            else
                return false;
        }

        #endregion
    }
}
